/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package simframe.simulation;

import java.util.*;
import java.util.concurrent.*;

import java.util.logging.Level;
import java.util.logging.Logger;

/*
 *
 * @Author Steve Noble
 *
 * Each Sim Thread keeps track of the local time
 * the rule is that each thread is allowed to run to the current global time
 * which is then stepped forward.
 * messages do no tadvance local time
 *  but actions do
 */
public class SimThread implements Runnable, SimActor {

    /*
     * Time Controller is th object that blocks
     */
    class SimThreadController implements Synchronizer.TimeController {


           /*
         * Time Controller Interface
         * Report time of next anticipated action
         */
          public long getNextTime() {
            if (actionMap.isEmpty()) {
                return Long.MAX_VALUE;
            }

            Long nextTime = actionMap.firstKey();

            return nextTime;
        }
      
        private long globalTime = 0;

        private synchronized void reset() {
            globalTime = 0;
        }

        /*
         * Block if running fast or if nothing to do
         * NOTE: wait is in loop so if notified
         * check will be performed again
         */
        private synchronized boolean checkActionQueue() throws InterruptedException {

            while (!checkNextActionTime() && doitNowQueue.isEmpty()) {
                // wait for it
                wait();
            }

            return true;
        }

        // general notify
        private synchronized void update() {
            notifyAll();
        }

        /*
         * Time Controller Interface
         * Notify all when the global time has advnaced
         */
        public synchronized void updateGlobalClockTime(long t) {
            globalTime = t;

            this.notifyAll();
        }
      

        // check if action on timed queue at global time
        // Next time should always be at or after global time
        // this is synchronized to avoid conflicts with global time
        private synchronized boolean checkNextActionTime() {

            if (actionMap.isEmpty()) {
                return false;
            }

            Long nextTime = actionMap.firstKey();

            assert( nextTime >= globalTime);

            return nextTime == globalTime;
        }

        private synchronized long getGlobalTime() {
            return globalTime;
        }
    }

    // private member data
    private Thread thread = null;
    private SortedMap<Long, Queue<PeriodicAction>> actionMap = new ConcurrentSkipListMap<Long, Queue<PeriodicAction>>();
    private Queue<Action> doitNowQueue = new ConcurrentLinkedQueue<Action>();

    // object we are running
    private final List<SimObject> simObjects = new ArrayList<SimObject>();

    // Control
    final SimThreadController control;
    final Simulation sim;
    private String debugName = null;

    SimThread(Simulation sim) {

        control = new SimThreadController();

        this.sim = sim;
    }

    public SimThread setDebug(String name) {
        debugName = name;
        
        return this;
    }

    public SimThread addSimObject(SimObject simObject) {

        simObject.setOwner(this);

        simObjects.add(simObject);

        return this;
    }

    private void debug(String message) {
        if (debugName == null) {
            return;
        }

        System.out.print(debugName + " " + message + "\n");
    }

    private void printQueue() {
        if (debugName == null) {
            return;
        }

        System.out.print("************************************************************************************ \n");
        debug("Queue Size: " + Integer.toString(doitNowQueue.size()));
        for (Long t : actionMap.keySet()) {
            Queue<PeriodicAction> actions = actionMap.get(t);
            debug("Action Queue Time: " + Time.toString(t) + "  Size: " + Integer.toString(actions.size()));
        }
        System.out.print("************************************************************************************ \n");
    }

     public void run() {

        try {
            debug("Start Sim Thread loop: " + Time.toString(control.getGlobalTime()));
            printQueue();
            while (!sim.stopped() && control.checkActionQueue()) {

                printQueue();

                while (!doitNowQueue.isEmpty()) {
                    Action x = doitNowQueue.poll();
                    x.exec();
                }

                while (control.checkNextActionTime()) {
                    // get next event off of wqueue
                    Long curTime = actionMap.firstKey();

                    Queue<PeriodicAction> actions = actionMap.get(curTime);

                   debug("At " + Time.toString(curTime) + " Found " + Integer.toString(actions.size()) + " Actions");
                   
                    while (!actions.isEmpty()) {
                        PeriodicAction action = actions.poll();

                        if (action != null) {

                            debug("Executing Action  Size: " + Integer.toString(actions.size()));

                            try {
                                action.x.exec();
                            } catch (Exception e) {
                                debug(e.getMessage());
                            }

                            if (action.period != null) {
                                long nextTime = action.period.getNext(curTime);

                                addAction(action, nextTime);
                            }
                        }
                    }
                   debug("Done Executing Actions\n");

                    // now remove curTime from synchronizer- this unblocks simulations
                    actionMap.remove(curTime);
                    sim.synchro.updateTime(control);
                }
            }
        } catch (InterruptedException ex) {
            Logger.getLogger(Simulation.class.getName()).log(Level.SEVERE, null, ex);
        }

        System.out.print("Exiting Thread - Synchro stopped\n");
    }

    private void addAction(PeriodicAction action, Long t) {

        if (action == null) {
            debug("attempting to add null action\n");
            return;
        }
        
        if (!actionMap.containsKey(t)) {
            actionMap.put(t, new ConcurrentLinkedQueue<PeriodicAction>());
        }

        Queue<PeriodicAction> actions = actionMap.get(t);
        actions.add(action);

        sim.synchro.updateTime(control);


        control.update();

        if (thread == null || !thread.isAlive()) {
            System.out.print("starting new Thread\n");
            start();
        }
    }

    // Actor interface
    public void action(Action x, long dt) {
        Long t = control.getGlobalTime() + dt;
        addAction(new PeriodicAction(x, null), t);
    }

    public void action(Action x, long dt, Time.Next periodicity) {
        Long t = control.getGlobalTime() + dt;
        addAction(new PeriodicAction(x, periodicity), t);
    }


    public void action(Action x) {
        doitNowQueue.add(x);

        control.update();
        
        if (thread == null || !thread.isAlive()) {

            System.out.print("starting new Thread\n");
            start();
        }

    }


    // SimObject inheritance
    public Long getTime() {
        return control.getGlobalTime();
    }

    public void start() {
        thread = new Thread(this);
        thread.start();
    }

    public void reset() {

        control.reset();

        actionMap.clear();

        for (SimObject simObject : simObjects) {
            simObject.reset();
        }

        sim.synchro.updateTime(control);
    }

}

